<!DOCTYPE html>
<html>
<!--
Copyright 2011 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<head>
<title>observeElement and observeAttribute Tests</title>
<script src="../third_party/closure/closure/goog/base.js"></script>
<script src="compat.js"></script>
<script src="weak_map.js"></script>
<script src="forwarding_handler.js"></script>
<script src="observers.js"></script>
<script src="dom_mutation.js"></script>
<script src="dom_observe.js"></script>
<script>
goog.require('goog.testing.jsunit');
</script>
</head>
<body>
<script>

function notReached(el) {
  fail('Called remove on ' + el);
}

function assertNextMutationIs(expected, log) {
  var mutation = log.clear()[0];
  if (!expected) {
    assertUndefined('mutation exists', mutation);
    return;
  }

  assertTrue('mutation does not exist', !!mutation);

  assertMutationEquals(expected, mutation);
}

function assertMutationEquals(expected, actual) {
  var count = 0;
  for (prop in expected) {
    if (Array.isArray(expected[prop]) && Array.isArray(actual[prop])) {
      assertArrayEquals('mutation prop ' + prop,
                        expected[prop], actual[prop]);
    } else {
      assertEquals('mutation prop ' + prop, expected[prop], actual[prop]);
    }
    count++;
  }

  assertEquals(count, Object.keys(actual).length);
}

// WebKit doesn't send DOMAttrModified events.
function browserSupportsAttributes() {
  var el = document.createElement('div');
  var sendsAttributeEvents = false;
  function attributeEventDetector(event) {
    sendsAttributeEvents = true;
  }
  el.addEventListener('DOMAttrModified', attributeEventDetector, false);
  el.setAttribute('id', 'foo');
  return sendsAttributeEvents;
}

// WebKit doesn't send mutation events when not attached to the document events.
function browserSupportsDetachedEvents() {
  var el = document.createElement('div');
  var sendsDetachedEvents = false;
  function eventDetector(event) {
    sendsDetachedEvents = true;
  }
  el.addEventListener('DOMNodeInserted', eventDetector, false);
  el.appendChild(document.createElement('span'));
  return sendsDetachedEvents;
}

function testAttributeChanged() {
  if (!browserSupportsAttributes()) return;

  var count = 0;
  var expected;
  function callback(mutation) {
    assertEquals(expected.element, mutation.element);
    assertEquals(expected.attribute, mutation.attribute);
    count++;
  }
  document.observeAttribute('span', 'id', callback);
  var span = document.createElement('span');
  document.body.appendChild(span);
  // Adding an element and setting an attribute
  // should cause the attribute setting to be swallowed.
  span.id = 'default';
  window.notifyObservers_();
  assertEquals(0, count);

  // Two sets should be collapsed into one.
  span.id = 'hell';
  span.id = 'hello';
  expected = {element: span, attribute: 'id'};
  window.notifyObservers_();
  assertEquals(1, count);

  span.id = 'goodbye';
  // Removing, then adding, should be ignored.
  document.body.removeChild(span);
  document.body.appendChild(span);
  window.notifyObservers_();
  assertEquals(2, count);

  span.removeAttribute('id');
  window.notifyObservers_();
  assertEquals(3, count);

  document.body.removeChild(span);
  window.notifyObservers_();
  assertEquals(3, count);

  var div = document.createElement('div');
  div.appendChild(span);
  document.body.appendChild(div);
  span.id = 'helloAgain';
  window.notifyObservers_();
  assertEquals(3, count);

  span.id = 'modification';
  document.body.removeChild(div);
  document.body.appendChild(div);
  window.notifyObservers_();
  assertEquals(4, count);

  document.stopObservingAttribute('span', 'id', callback);
  span.id = 'shouldnotbeobserved';
  window.notifyObservers_();
  assertEquals(4, count);

  document.body.removeChild(div);
}

function testAttributeChangedNotFiredOnRemove() {
  if (!browserSupportsAttributes()) return;

  var count = 0;
  function callback(mutation) {
    count++;
  }

  document.observeAttribute('span', 'id', callback);
  var div = document.createElement('div');
  var span = document.createElement('span');
  div.appendChild(span);
  document.body.appendChild(div);
  window.notifyObservers_();
  assertEquals(0, count);

  div.removeChild(span);
  span.id = 'hello';
  window.notifyObservers_();
  assertEquals(0, count);

  div.appendChild(span);
  window.notifyObservers_();
  assertEquals(0, count);

  span.id = 'goodbye';
  document.body.removeChild(div);
  window.notifyObservers_();
  assertEquals(0, count);
}

function testElementMutations() {
  var expectedInserted = undefined;
  var expectedRemoved = undefined;
  var insertCount = 0;
  var removeCount = 0;
  function inserted(mutation) {
    assertEquals('ElementInserted', mutation.type);
    assertEquals(expectedInserted, mutation.element);
    insertCount++;
  };
  function removed(mutation) {
    assertEquals('ElementRemoved', mutation.type);
    assertEquals(expectedRemoved, mutation.element);
    removeCount++;
  };
  document.observeElementInserted('span', inserted);
  document.observeElementRemoved('span', removed);
  var span = document.createElement('span');
  var div = document.createElement('div');
  div.appendChild(span);
  document.body.appendChild(div);
  expectedInserted = span;
  window.notifyObservers_();
  assertEquals(1, insertCount);
  assertEquals(0, removeCount);

  div.removeChild(span);
  expectedRemoved = span;
  window.notifyObservers_();
  assertEquals(1, insertCount);
  assertEquals(1, removeCount);

  document.stopObservingElementInserted('span', inserted);
  document.stopObservingElementRemoved('span', removed);
  div.appendChild(span);
  window.notifyObservers_();
  assertEquals(1, insertCount);
  assertEquals(1, removeCount);

  document.body.removeChild(div);
}

function testSubtreeRemovalBehavior() {
  if (!browserSupportsDetachedEvents()) return;

  var div1 = document.createElement('div');
  var div2 = document.createElement('div');
  var span = document.createElement('span');
  div1.appendChild(div2);
  div2.appendChild(span);
  document.body.appendChild(div1);

  var count = 0;
  var expected = span;
  function removed(mutation) {
    count++;
    assertEquals(expected, mutation.element);
  }
  document.observeElementRemoved('span', removed);

  document.body.removeChild(div1);
  // This add will be ignored since this is a detached subtree.
  div1.appendChild(document.createElement('span'));
  div1.removeChild(div2);
  div2.removeChild(span);
  // This add will be ignored because it was previously
  // marked as a "remove" (when div1 is queried for its descendants).
  div1.appendChild(document.createElement('span'));
  window.notifyObservers_();
  assertEquals(1, count);
}

function testSubtreeMoveBehavior() {
  if (!browserSupportsDetachedEvents()) return;

  var count = 0;
  var expected;
  function callback(mutation) {
    assertEquals(expected, mutation.element);
    count++;
  }
  document.observeElementInserted('span', callback);
  document.observeElementRemoved('span', notReached);
  var div = document.createElement('div');
  document.body.appendChild(div);
  window.notifyObservers_();
  assertEquals(0, count);

  document.body.removeChild(div);
  var span = document.createElement('span');
  div.appendChild(span);
  document.body.appendChild(div);
  expected = span;
  window.notifyObservers_();
  assertEquals(1, count);

  document.stopObservingElementInserted('span', callback);
  document.stopObservingElementRemoved('span', notReached);
}

function testSubtreeMoveBehavior2() {
  var count = 0;
  var expected;
  function callback(mutation) {
    assertEquals(expected, mutation.element);
    count++;
  }
  document.observeElementInserted('span', callback);
  document.observeElementRemoved('span', callback);
  var div = document.createElement('div');
  var span = document.createElement('span');
  document.body.appendChild(div);
  document.body.appendChild(span);
  expected = span;
  window.notifyObservers_();
  assertEquals(1, count);

  document.body.removeChild(span);
  var div2 = div.appendChild(document.createElement('div'));
  div2.appendChild(span);
  window.notifyObservers_();
  assertEquals(1, count);

  document.stopObservingElementInserted('span', callback);
  document.stopObservingElementRemoved('span', notReached);
}

function testAddingToDetachedSubtree() {
  var div = document.createElement('div');
  document.body.appendChild(div);

  document.observeElementInserted('b', notReached);
  document.observeElementInserted('b', notReached);
  var span = document.createElement('span');
  var b = document.createElement('b');
  document.body.removeChild(div);
  div.appendChild(span);
  span.appendChild(b);
  window.notifyObservers_();
}

function testRemoveThenAddElsewhere() {
  var div1 = document.createElement('div');
  var div2 = document.createElement('div');
  var span = document.createElement('span');
  div1.appendChild(span);
  div1.appendChild(div2);
  document.body.appendChild(div1);

  var count = 0;
  var expected;
  function callback(mutation) {
    assertEquals(expected, mutation.element);
    count++;
  }

  document.observeElementInserted('span', notReached);
  document.observeElementRemoved('span', callback);
  div1.removeChild(span);
  div1.removeChild(div2);
  div2.appendChild(span);
  expected = span;
  window.notifyObservers_();
  assertEquals(1, count);

  document.stopObservingElementInserted('span', notReached);
  document.stopObservingElementRemoved('span', callback);
}

function testMultipleElementObservations() {
  var count = 0;
  var expected = [];
  function callback(mutation) {
    assertEquals(expected[count], mutation.element);
    count++;
  }
  document.observeElementInserted('em', callback);
  document.observeElementInserted('strong', callback);

  var em = document.createElement('em');
  var strong = document.createElement('strong');
  var div = document.createElement('div');
  div.appendChild(strong);
  div.appendChild(em);
  document.body.appendChild(div);
  // Order is based on the observers, not the order
  // of mutations.
  expected.push(em, strong);
  window.notifyObservers_();
  assertEquals(2, count);

  document.stopObservingElementInserted('em', callback);
  document.stopObservingElementInserted('strong', callback);
}

function testMultipleAttributeObservations() {
  if (!browserSupportsAttributes()) return;

  var count = 0;
  var expected = [];
  function callback(mutation) {
    assertEquals(expected[count].element, mutation.element);
    assertEquals(expected[count].attribute, mutation.attribute);
    count++;
  }
  document.observeAttribute('*', 'id', callback);
  document.observeAttribute('strong', 'data-foo', callback);
  var div = document.createElement('div');
  var strong = document.createElement('strong');
  div.appendChild(strong);
  document.body.appendChild(div);
  window.notifyObservers_();
  assertEquals(0, count);

  div.id = 'divId';
  strong.id = 'strongId';
  strong.setAttribute('data-foo', 'something');
  expected.push({element: div, attribute: 'id'});
  expected.push({element: strong, attribute: 'id'});
  expected.push({element: strong, attribute: 'data-foo'});
  window.notifyObservers_();
  assertEquals(3, count);
}

</script>
</body>
</html>
